'''
Created on Nov 12, 2011

@author: Victor Bos
'''

import math
import re
import StringIO

import Area
import graphics
import io
import utils

class AreaInfo:
    # This class stores game information about areas

    def __init__(self):
        self.areas = {} # mapping from area names to Areas
        self.neighbours = set()# set of frozensets of names of neighbouring areas
        self.areas_bbox = (0, 0, 0, 0)

    def __copy__(self):
        ainfo = AreaInfo()
        ainfo.areas = utils.copydict(self.areas)
        ainfo.neighbours = utils.copyset(self.neighbours)
        ainfo.areas_bbox = self.areas_bbox
        return ainfo

    def update_areas_bbox(self, area_name=None):
        if area_name is None:
            areas = self.areas.values()
            # re-initialize the current bbox
            self.reset_areas_bbox()
        else:
            areas = [self.areas[area_name]]
            # no re-initialization needed here

        if self.areas_bbox is None:
            self.areas_bbox = (0, 0, 0, 0)

        if len(areas) > 0:
            bbox = list(self.areas_bbox)
            for area in areas:
                x0, y0, x1, y1 = area.bbox()
                if x0<bbox[0]:
                    bbox[0] = x0
                if x1>bbox[2]:
                    bbox[2] = x1
                if y0<bbox[1]:
                    bbox[1] = y0
                if y1>bbox[3]:
                    bbox[3] = y1
            self.areas_bbox = tuple(bbox)
    
    def get_areas_bbox(self):
        if self.areas_bbox is None:
            self.update_areas_bbox()
        return self.areas_bbox

    def reset_areas_bbox(self):
        self.areas_bbox = None

    def has_area(self, aname):
        return aname in self.areas

    def num_areas(self):
        return len(self.areas)

    def has_areas(self):
        return len(self.areas)>0

    def get_areas(self):
        return self.areas.values()
    
    def get_area_names(self):
        return self.areas.keys()
    
    def add_area(self, area, nbs = None):
        '''Adds a new area. If an area with area.name exists, it is
           overwritten
        '''
        if self.has_area(area.name):
            self.remove_area(area.name)
        self.areas[area.name] = area
        self.update_areas_bbox(area_name = area.name)
        if nbs is None:
            nbs = self.compute_neighbours(area.name)
        for nb in nbs:
            self.make_neighbours(area.name, nb)

    def compute_neighbours(self, area_name):
        nbs = []
        area0 = self.areas[area_name]        
        for aname in self.areas:
            if aname != area_name:
                area1 = self.areas[aname]
                if Area.overlapping_areas(area0, area1):
                    nbs.append(aname)
        return nbs

    def recompute_neighbours(self, aname):
        self.remove_neighbours(aname)
        nbs = self.compute_neighbours(aname)
        for nb in nbs:
            self.make_neighbours(aname, nb)
        
    def make_area(self, attrs):
        area = None
        neighbours = None
        area_name = attrs["name"]
        del attrs["name"]
        gpoint = None
        if "gpoint" in attrs:
            gpoint = eval(attrs["gpoint"])
            del attrs["gpoint"]
        if "neighbours" in attrs:
            neighbours = [nb.strip() for nb in attrs["neighbours"].split("\n")]
            del attrs["neighbours"]
        polygon = []

        # "outline" is key if read from file
        if attrs.has_key("outline"):
            points = [p for p in attrs["outline"].split("\n")]
            points = [p.split(",") for p in points ]
            polygon = [ (int(x), int(y)) for x,y in points]
            del attrs["outline"]

        # "polygon" is key if created in area editor
        if attrs.has_key("polygon"):
            polygon = attrs["polygon"]
            del attrs["polygon"]
            
        if len(polygon) > 0:
            area = Area.Area(area_name, polygon)
            area.attrs.update(attrs)
            if gpoint is not None:
                area.gpoint = gpoint
                
        return area, neighbours
        
    def remove_area(self, area_name):
        if self.has_area(area_name):
            # remove from neighbours
            excluded = set()
            for nbfs in self.neighbours:
                if area_name in nbfs:
                    excluded.add(nbfs)
            self.neighbours.difference_update(excluded)
            # remove from area_name and area_cid
            area = self.areas[area_name]
            # reset areas bbox if needed
            abbox = area.bbox()
            asbbox = self.get_areas_bbox()
            for i in range(4):
                if abbox[i] == asbbox[i]:
                    self.reset_areas_bbox()
            del self.areas[area_name]

    def get_new_area_name(self):
        n = 0
        area_name = "Area-%d" % n
        while area_name in self.areas:
            n += 1
            area_name = "Area-%d" % n
        return area_name

    def make_neighbours(self, area1_name, area2_name):        
        self.neighbours.add(frozenset([area1_name, area2_name]))
        
    def make_not_neighbours(self, area1_name, area2_name):
        self.neighbours.discard(frozenset([area1_name, area2_name]))

    def remove_neighbours(self, area_name):
        self.neighbours = set([nbfs for nbfs in self.neighbours if area_name not in nbfs])

    def get_neighbours(self, area_name):
        result = []
        for nbfs in self.neighbours:
            if area_name in nbfs:
                result.extend([nb for nb in nbfs if nb != area_name])
        return result

    def are_neighbours(self, aname1, aname2):
        return aname2 in self.get_neighbours(aname1)
    
    def rename_area(self, oldname, newname):
        if self.has_area(oldname) and not self.has_area(newname):
            area = self.areas[oldname]
            nbs = self.get_neighbours(oldname)
            for nb in nbs:
                self.make_not_neighbours(oldname, nb)
                self.make_neighbours(newname, nb)
            area.name = newname
            del self.areas[oldname]
            self.areas[area.name] = area

    def move_area_polygon_start(self, aname, x, y):
        area = self.areas[aname]
        oldx, oldy = area.poly_start_point()
        area.set_poly_start_point(x, y)
        area.reset_bbox()
        x0, y0, x1, y1 = self.get_areas_bbox()
        if (not graphics.in_bbox(x, y, x0, y0, x1, y1) or
            (oldx==x0 or oldx==x1 or oldy==y0 or oldy==y1)):
            self.reset_areas_bbox()
        self.recompute_neighbours(aname)
        
    def get_areas_file_content(self):
        buf = StringIO.StringIO()
        area_names = self.get_area_names()
        area_names.sort()
        sep = ""
        for area_name in area_names:
            area = self.areas[area_name]
            buf.write(sep + "begin Area\n")
            buf.write("  name: %s\n" % area_name)

            nbs = self.get_neighbours(area_name)
            if len(nbs)>0:
                buf.write("  neighbours:\n    ")
                buf.write("\n    ".join(nbs))
                buf.write("\n")    

            if len(area.attrs)>0:
                io.write_attrs(area.attrs, buf, "  ")
                buf.write("\n")
            
            coords = area.polygon
            buf.write("  outline:\n")
            i = 0
            while i<len(coords):
                buf.write("    %d, %d\n" % (coords[i][0], coords[i][1]))
                i += 1

            buf.write("  gpoint: (%d, %d)\n" % area.gravity_point())
            buf.write("end Area\n")
            sep = "\n"
        result = buf.getvalue()
        buf.close()
        return result
    
    def save_areas(self, filename):
        try:
            ofile = file(filename, "w")
            ofile.write(self.get_areas_file_content())
            ofile.close()
        except IOError:
            return False
        return True

    def load_areas_file_content(self, areas_str):
        if self.is_old_style_areas(areas_str):
            areas_str = self.convert_old_style_areas(areas_str)
        lines = utils.read_stripped_lines(StringIO.StringIO(areas_str), eolstr="")
        attr_lines = []        
        for l in lines:
            if len(l)>0:
                if l.startswith("end Area"):
                    area_attrs = io.read_attrs("\n".join(attr_lines))
                    if not area_attrs.has_key("name"):
                        return False
                    elif self.has_area(area_attrs["name"]):
                        return False
                    else:
                        area, neighbours = self.make_area(area_attrs)
                        if area is not None:
                            self.add_area(area, neighbours)
                        else:
                            return False
                elif l.startswith("begin Area"):
                    attr_lines = []
                else:
                    attr_lines.append(l)
        return True

    def is_old_style_areas(self, strbuffer):
        rexp = re.compile(r"begin Area\s*\n")
        result = rexp.search(strbuffer) is None
        # uncomment for debug
        #if result:
        #    print "old style area file"
        return result
    
    def convert_old_style_areas(self, strbuffer):
        rexp = re.compile(r"begin\s+(?P<aname>[^\n]*)\n")
        strbuffer = rexp.sub("begin Area\n  name: \g<aname>\n", strbuffer)
        rexp = re.compile(r"end\s+[^\n]*\n")
        strbuffer = rexp.sub(r"end Area\n", strbuffer)
        
        # insert outline: keys if needed
        rexp = re.compile(r"(?P<nameline>\s*name:[^\n]+\n)(?P<point>\s*[0-9]+,\s*[0-9]+\s*\n)", re.MULTILINE)
        strbuffer = rexp.sub(r"\g<nameline>  outline:\n\g<point>", strbuffer)
        return strbuffer
                          
    def load_areas(self, filename):
        try:
            inputfile = file(filename, "r")
        except IOError, ioe:
            return False
        strbuffer = inputfile.read()
        inputfile.close()
        result = self.load_areas_file_content(strbuffer)
        return result

    def next_area(self, area, delta):
        areas = self.get_areas()
        areas.sort(self.cmp_area_loc)
        i = areas.index(area)
        if i >- 1:
            i += delta
            if i==len(areas):
                i = 0
            elif i == -1:
                i = len(areas)-1
            return areas[i]
        return area

    def cmp_area_loc(self, a0, a1):

        def mcmp(a, b, m):
            if a<b-m:
                return -1
            elif b<a-m:
                return 1
            return 0

        llx, lly, urx, ury = self.get_areas_bbox()
        d = self.num_areas()
        margin_x = (urx - llx) / d
        margin_y = (ury - lly) / math.sqrt(d)
        x0, y0 = a0.gravity_point()
        x1, y1 = a1.gravity_point()
        v = mcmp(y0, y1, margin_y)        
        if v == 0:
            v = mcmp(x0, x1, margin_x)
        return v

